package nf;

import java.util.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.awt.Dimension;
import java.awt.BorderLayout;
import java.awt.event.*;
import javax.swing.*;
import nf.graph.Graph;
import nf.graph.Group;
import nf.graph.UndirectedGraph;
import nf.graph.Delta;
import nf.graph.Node;
import nf.graph.persistence.GraphReader;
import nf.graph.persistence.GraphReadException;
import nf.graph.persistence.ParseException;
import nf.traversal.*;
import nf.utility.*;
import nf.util.Rational;
import nf.util.ArgumentParser;
import nf.util.ArgumentParseException;

public class Simulator
{
	private static final Map<String, TraverserFactory> traverserFactories =
		new HashMap<String, TraverserFactory>();

	private Simulator()
	{
		throw new UnsupportedOperationException("static class");
	}
	
	private static class GraphPrinter extends GameRunListener.Adapter
	{
		private FormationGame game;
		private Graph network;
	
		public GraphPrinter(FormationGame game, Graph network)
		{
			this.game = game;
			this.network = network;
		}
	
		public void runStarted(GameRunner runner)
		{
			Simulator.printNetwork(game, network);
		}
		
		public void stepFinished(GameRunner runner)
		{
			Simulator.printNetwork(game, network);
		}
		
		public void equilibriumReached(GameRunner runner)
		{
			System.out.println("Done.");
		}
	}
	
	private static class GraphDisplayer extends GameRunListener.Adapter
	{
		private GameRunner runner;
		private FormationGame game;
		private Graph network;
		private GraphDisplay display;
		private boolean showDensity;
		private boolean interrupting;
		private boolean autoAnimate;
		private boolean equilibrium;
	
		public GraphDisplayer(GameRunner runner, FormationGame game,
			boolean showDensity, boolean autoAnimate)
		{
			this.runner = runner;
			this.game = game;
			this.network = runner.getNetwork();
			this.showDensity = showDensity;
			this.autoAnimate = autoAnimate;
			this.interrupting = true;
			this.equilibrium = false;
			
			this.display = new GraphDisplay(network,
				game.getUtilityCalculator());
			
			javax.swing.SwingUtilities.invokeLater(new Runnable() {
				public void run()
				{
					showWindow();
				}
			});
		}
		
		private void showWindow()
		{
			JFrame frame = new JFrame("Network Formation Simulation");
			frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
			
			display.setPreferredSize(new Dimension(530, 530));
			frame.addKeyListener(new Commander());
			display.showAverageDegree(showDensity);
			display.addAnimationListener(new Animation());
			
			frame.getContentPane().add(display, BorderLayout.CENTER);
			frame.pack();
			frame.setVisible(true);
		}
		
		public void runStarted(GameRunner r)
		{
			if (autoAnimate)
				animate();
			if (interrupting)
				runner.interrupt();
		}
		
		public void stepFinished(GameRunner r)
		{
			if (autoAnimate) {
				animate();
				runner.interrupt();
			} else if (interrupting) {
				runner.interrupt();
			}
			Thread.yield();
		}
		
		public void equilibriumReached(GameRunner r)
		{
			equilibrium = true;
			display.setCompleted();
		}
		
		private boolean isInterrupted()
		{
			return (runner.getState() == GameRunner.State.INTERRUPTED);
		}
		
		private void animate()
		{
			display.animatePhysics();
		}
		
		private class Animation implements GraphDisplay.AnimationListener
		{
			public void animationDone()
			{
				if (!interrupting && isInterrupted() && !equilibrium) {
					runner.resume();
				}
			}
		}
		
		private class Commander extends KeyAdapter
		{
			public void keyTyped(KeyEvent e)
			{
				char c = Character.toLowerCase(e.getKeyChar());
				
				switch (c) {
					case 'r':
						if (interrupting) {
							interrupting = false;
							if (isInterrupted())
								runner.resume();
						}
						break;
					case 's':
						if (interrupting && isInterrupted()) {
							runner.resume();
						}
						break;
					case 'b':
						if (!interrupting) {
							interrupting = true;
						}
						break;
					case 'a':
						display.runPhysicsUntilDone();
						break;
					case 'q':
						animate();
						break;
				}
			}
		}
	}
	
	public static Graph readNetwork(String filename)
	{
		return readNetwork(new File(filename));
	}
	
	public static Graph readNetwork(File file)
	{
		try {
			return (new GraphReader(file)).read();
		} catch (FileNotFoundException e) {
			System.err.printf("graph file \"%s\" not found%n", file);
			System.exit(-1);
			return null;
		} catch (GraphReadException e) {
			try {
				ParseException pe = (ParseException) e.getCause();
				if (pe != null) {
					pe.printStackTrace();
					System.err.printf("parse error on line %d: %s%n",
						pe.getLineNumber(), pe.getLocalizedMessage());
					System.exit(-1);
				}
			} catch (ClassCastException cce) {
				// that's fine, we'll just use default handling
			}
			
			System.err.printf("error: %s%n", e);
			System.exit(-1);
			return null;
		}
	}
	
	public static void setEdgeCost(Graph graph, Rational newCost)
	{
		setEdgeCost(graph.getRootGroup(), newCost);
	}
	
	private static void setEdgeCost(Group group, Rational cost)
	{
		try {
			((OrganizationalGroup) group).setEdgeCost(cost);
		} catch (ClassCastException e) {
			group.setProperty("edgeCost", cost);
		}
		
		for (Group child : group.getChildGroups())
			setEdgeCost(child, cost);
	}
	
	public static TraverserFactory getTraverserFactory(String name)
	{
		if (traverserFactories.isEmpty()) {
			TraverserFactory f;
			
			f = new OrderedPairwiseTraverser.Factory();
			for (String n : new String[] {"sorted", "default", "ordered"}) {
				traverserFactories.put(n, f);
			}
			
			f = new RandomizedPairwiseTraverser.Factory();
			for (String n : new String[] {"random", "randomized"}) {
				traverserFactories.put(n, f);
			}
			
			f = new FairPairwiseTraverser.Factory();
			for (String n : new String[] {"feedback", "fair"}) {
				traverserFactories.put(n, f);
			}
			
			f = new RandomizedFairPairwiseTraverser.Factory();
			for (String n : new String[] {"random-fair", "fair-random"}) {
				traverserFactories.put(n, f);
			}
			
			f = new PairwiseTraverser.Factory();
			for (String n : new String[] {"arbitrary", "natural"}) {
				traverserFactories.put(n, f);
			}
		}
		
		TraverserFactory factory = traverserFactories.get(name.toLowerCase());
		if (factory == null) {
			throw new IllegalArgumentException("Unknown traverser '" + name +
				"'.");
		}
		
		return factory;
	}
	
	public static UtilityCalculator getUtilityCalculator(String name)
	{
		if (name.equals("default") || name.equals("simple"))
			return new SimpleUtilityCalculator();
		else if (name.equals("infantile") || name.equals("very-simple"))
			return new InfantileUtilityCalculator();
		else if (name.equals("complex") || name.equals("super"))
			return new ComplexUtilityCalculator();
		else
			throw new IllegalArgumentException("Unknown utility calculator '" +
				name + "'.");
	}

	public static void main(String... args)
	{
		Graph network;
		ArgumentParser p = new ArgumentParser();
		ArgumentParser.Result r;
		Object overrideCost;
		
		p.addOption("autoAnimate", "--auto-animate").setImplicitValue(true)
			.setDefaultValue(false);
		p.addOption("cost", "-c", "--edge-cost").setValueType(Rational.class);
		p.addOption("traverser", "-t", "--traverser").setValueType(String.class)
			.setDefaultValue("ordered");
		p.addOption("utility", "-u", "--util-calc").setValueType(String.class)
			.setDefaultValue("simple");
		p.addOption("quiet", "-q", "--quiet").setImplicitValue(true)
			.setDefaultValue(false);
		p.addOption("gui", "-G", "--no-gui").setImplicitValue(false)
			.setDefaultValue(true);
		p.addOption("density", "-d", "--show-density").setImplicitValue(true)
			.setDefaultValue(false);
		p.addOption("bounds", "-b", "--cost-bounds").setImplicitValue(true)
			.setDefaultValue(false);
			
		try {
			r = p.parse(args);
		} catch (ArgumentParseException e) {
			System.err.printf("err: %s%n", e.getLocalizedMessage());
			System.exit(-1);
			return;
		}
		
		if (r.args.length <= 0) {
			System.err.println("error: must provide a graph filename");
			System.exit(-1);
			return;
		}
		String filename = r.args[0];
		
		try {
			network = readNetwork(filename);
			overrideCost = r.options.get("cost");
			if (overrideCost != null)
				setEdgeCost(network, (Rational) overrideCost);
			
			FormationGame game = new FormationGame(
				getTraverserFactory((String) r.options.get("traverser")),
				getUtilityCalculator((String) r.options.get("utility"))
			);
			GameRunner runner = new GameRunner(game, network);
			
			if (r.options.get("bounds") == Boolean.TRUE)
				findCostBounds(game, network.copy());
			
			if (r.options.get("quiet") != Boolean.TRUE)
				runner.addListener(new GraphPrinter(game, network));
			if (r.options.get("gui") != Boolean.FALSE) {
				GraphDisplayer gd = new GraphDisplayer(runner, game,
					r.options.get("density") == Boolean.TRUE,
					r.options.get("autoAnimate") == Boolean.TRUE);
				runner.addListener(gd);
			}
			
			runner.run();
		} catch (InvalidGameGraphException e) {
			System.err.printf("inappropriate graph: %s%n", e.getMessage());
			System.exit(-1);
		}
	}
	
	private static void findCostBounds(FormationGame game, Graph network)
	{
		List<Rational> bounds = game.getCostBounds(network);
		Rational lower = bounds.get(0);
		Rational upper = bounds.get(1);
		
		System.out.printf("Cost bounds: %s (%.3f) | %s (%.3f) ==> %s%n",
			lower, lower.doubleValue(),
			upper, upper.doubleValue(),
			((lower.compareTo(upper) <= 0)? "stable" : "never stable")
		);
	}
	
	public static void printNetwork(EdgeGame game, Graph network)
	{
		List<Node> nodes = new ArrayList<Node>(network);
		List<Node> neighbors;
		Map<Node,Rational> utils = game.getUtilities(network);
		
		Collections.sort(nodes);
		int maxNameLength = 0;
		
		int i = 1;
		for (Node n : nodes) {
			String name = n.getName();
			int length;
			if (name == null)
				name = String.format("(null #%d)", i++);
			
			length = name.length();
			if (length > maxNameLength)
				maxNameLength = length;
		}
		
		for (Node n : nodes) {
			String name = n.getName();
			char[] spacing = new char[maxNameLength - name.length() + 1];
			Arrays.fill(spacing, ' ');
			System.out.printf("%s%s(u=%s)  -> ", n.getName(),
				new String(spacing), utils.get(n));
			
			neighbors = network.getNeighbors(n);
			if (neighbors.isEmpty()) {
				System.out.println("(no edges)");
			} else {
				for (Node neighbor : neighbors) {
					System.out.printf("%s ", neighbor.getName());
				}
				System.out.println();
			}
		}
		System.out.println("---------------------------");
	}
}
